/* Turska Framework/UI library (Turs2)
 * Copyright 2003-2009 Jetro Lauha
 * All rights reserved.
 * Web: http://iki.fi/jetro/turska/
 *
 * This library is free software; you can redistribute it and/or
 * modify it under the terms of EITHER:
 *   (1) The GNU Lesser General Public License as published by the Free
 *       Software Foundation; either version 2.1 of the License, or (at
 *       your option) any later version. The text of the GNU Lesser
 *       General Public License is included with this library in the
 *       file LICENSE-LGPL.txt.
 *   (2) The BSD-style license that is included with this library in
 *       the file LICENSE-BSD.txt.
 *
 * This library is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the files
 * LICENSE-LGPL.txt and LICENSE-BSD.txt for more details.
 *
 * $Id: T2GraphicsIrr.cpp 131 2009-06-05 05:58:05Z jlauha $
 * $Revision: 131 $
 */

#include "Turs2.h"

#ifdef T2_USE_IRR

#include <assert.h>

using namespace irr;

namespace turska
{


static inline UI32 sNextPowerOfTwo(UI32 input)
{
     --input;
     input |= input >> 16;
     input |= input >> 8;
     input |= input >> 4;
     input |= input >> 2;
     input |= input >> 1;
     return input + 1;
}


GraphicsIrr::GraphicsIrr(irr::IrrlichtDevice *device) :
    mIrrDevice(device),
    mDefaultFlags(IF_DEFAULT_FLAGS)
{
    mDefaultMaterial.MaterialType = video::EMT_ONETEXTURE_BLEND;
    mDefaultMaterial.MaterialTypeParam = video::pack_texureBlendFunc(video::EBF_SRC_ALPHA, video::EBF_ONE_MINUS_SRC_ALPHA, video::EMFN_MODULATE_1X);
    mDefaultMaterial.setFlag(video::EMF_LIGHTING, false);
    mDefaultMaterial.setFlag(video::EMF_ZBUFFER, false);
    mDefaultMaterial.setFlag(video::EMF_ZWRITE_ENABLE, false);
    mDefaultMaterial.setFlag(video::EMF_BACK_FACE_CULLING, false);
    mDefaultMaterial.setFlag(video::EMF_BILINEAR_FILTER, false);    // default
    mDefaultMaterial.setFlag(video::EMF_TRILINEAR_FILTER, false);   // default
    /* setFlag in API is just for bool in Irr 1.4, so we're setting the wrap manually
     //mDefaultMaterial.setFlag(video::EMF_TEXTURE_WRAP, video::ETC_CLAMP_TO_EDGE);
     * Although clamping is configured here, there still was problems
     * getting it actually working in some tests. Because of that,
     * there's currently extra code to manually inset a pixel border
     * in the image itself.
     */
    for (u32 a = 0; a < video::MATERIAL_MAX_TEXTURES; ++a)
        mDefaultMaterial.TextureLayer[a].TextureWrap = video::ETC_CLAMP_TO_EDGE;
}


void GraphicsIrr::beginRender()
{
    video::IVideoDriver *driver = mIrrDevice->getVideoDriver();

    core::matrix4 identity;
    driver->setTransform(video::ETS_WORLD, identity);
    driver->setTransform(video::ETS_VIEW, mInitialViewMatrix);

    core::matrix4 projection;
    const irrdimension2d &screenSize = driver->getCurrentRenderTargetSize();
    projection.buildProjectionMatrixOrthoLH((f32)screenSize.Width, -(f32)screenSize.Height, -1, 1);

    video::E_DRIVER_TYPE driverType = driver->getDriverType();
    // hack: with Irrlicht v1.4 the translation must be set differently depending on video driver
    if (driverType == video::EDT_OPENGL)
        projection.setTranslation(core::vector3df(1, 1, 0));
#ifdef _IRR_COMPILE_WITH_OGLES1_
    else if (driverType == video::EDT_OGLES1)
        projection.setTranslation(core::vector3df(1, 1, 0));
#endif // _IRR_COMPILE_WITH_OGLES1_
    else if (driverType == video::EDT_DIRECT3D8 ||
             driverType == video::EDT_DIRECT3D9)
        // D3D samples pixels at (0.5,0.5) so set the projection offset for it here
        projection.setTranslation(core::vector3df(-1 - 1.0f / screenSize.Width,
                                                  1 + 1.0f / screenSize.Height, 0));
    else if (driverType == video::EDT_SOFTWARE)
        projection.setTranslation(core::vector3df(-1, 1, 0));
    else if (driverType == video::EDT_BURNINGSVIDEO)
    {
        // burning's software doesn't seem to work with re-setting driver transform
        assert(0 && "Burning's video driver isn't supported in Turska GraphicsIrr");
    }
    driver->setTransform(video::ETS_PROJECTION, projection);

    // Image drawing will reset material
    //driver->setMaterial(mDefaultMaterial);
}


void GraphicsIrr::endRender()
{
}


Image * GraphicsIrr::loadImage(const char *fileName, UI32 flags)
{
    video::IImage *image = loadIrrImage(fileName);
    if (image == 0)
        return 0;
    return createImage(fileName, image, true, flags);
}


Image * GraphicsIrr::loadImage(const char *name, irr::io::IReadFile *file, UI32 flags)
{
    video::IImage *image = loadIrrImage(file);
    if (image == 0)
        return 0;
    return createImage(name, image, true, flags);
}


Image * GraphicsIrr::createImage(const char *name, irr::video::IImage *image,
                                 bool dropImage, UI32 flags, PixelRectangle *area)
{
    if (flags == G_USE_DEFAULT_FLAGS)
        flags = mDefaultFlags;

    video::ITexture *texture = 0;
    UI32 width = image->getDimension().Width;
    UI32 height = image->getDimension().Height;
    if (area)
    {
        width = area->width;
        height = area->height;
    }
    core::vector2df uv[2];
    uv[0].set(0, 0);
    uv[1].set(1.f, 1.f);
    bool success = createTexture(name, texture, uv, image, flags, area);
    if (!success)
        return 0;
    if (dropImage)
        image->drop();
    return new ImageIrr(this, width, height, flags, texture, uv);
}


video::IImage * GraphicsIrr::loadIrrImage(const char *fileName)
{
    irr::video::IVideoDriver *driver = mIrrDevice->getVideoDriver();
    return driver->createImageFromFile(fileName);
}


video::IImage * GraphicsIrr::loadIrrImage(irr::io::IReadFile *file)
{
    return mIrrDevice->getVideoDriver()->createImageFromFile(file);
}


static void sFixAlphaAndBorder_Irr(irr::video::IVideoDriver *driver,
                                   irr::video::IImage *image, bool insetBorderClamp,
                                   UI32 areaWidth, UI32 areaHeight)
{
    if (insetBorderClamp)
    {
        areaWidth += 2;
        areaHeight += 2;
    }
    const turska::GraphicsIrr::irrdimension2d &imageSize = image->getDimension();
    if (imageSize.Width < 2 || imageSize.Height < 2 || areaWidth < 2 || areaHeight < 2)
        return;

    if (image->getAlphaMask() == 0)
        return; // completely opaque

    void *lockedData = image->lock();
    if (lockedData == 0)
        return;

    s32 x, y;
    const u32 alphaMask = image->getAlphaMask();
    const u32 redMask = image->getRedMask();
    const u32 greenMask = image->getGreenMask();
    const u32 blueMask = image->getBlueMask();
    u32 redShift, greenShift, blueShift;

    switch (image->getColorFormat())
    {
    default:
        assert(0 && "Unsupported color format in sFixAlphaAndBorder_Irr");
        /* fall through, assume 32bpp */
    case video::ECF_A8R8G8B8:
        redShift = 16;
        greenShift = 8;
        blueShift = 0;
        break;
    case video::ECF_A1R5G5B5:
        redShift = 10;
        greenShift = 5;
        blueShift = 0;
        break;
    case video::ECF_R5G6B5:
        redShift = 11;
        greenShift = 5;
        blueShift = 0;
        break;
    case video::ECF_R8G8B8:
        redShift = 16;
        greenShift = 8;
        blueShift = 0;
        break;
    }


#define PROCESS_IMAGE(pixel_type) \
        pixel_type *pixels = (pixel_type *)lockedData; \
        I32 co = 0; \
        for (y = 0; y < (I32)areaHeight; ++y) \
        { \
            for (x = 0; x < (I32)areaWidth; ++x) \
            { \
                if ((pixels[co] & alphaMask) == 0) \
                { \
                    /* iterate through 3x3 window around pixel */ \
                    I32 left = x - 1, right = x + 1, top = y - 1, bottom = y + 1; \
                    if (left < 0) left = 0; \
                    if (right >= (I32)areaWidth) right = areaWidth - 1; \
                    if (top < 0) top = 0; \
                    if (bottom >= (I32)areaHeight) bottom = areaHeight - 1; \
                    I32 x2, y2, colors = 0, co2 = top * imageSize.Width + left; \
                    I32 red = 0, green = 0, blue = 0; \
                    for (y2 = top; y2 <= bottom; ++y2) \
                    { \
                        for (x2 = left; x2 <= right; ++x2) \
                        { \
                            pixel_type px = pixels[co2++]; \
                            if (px & alphaMask) \
                            { \
                                red += (px & redMask) >> redShift; \
                                green += (px & greenMask) >> greenShift; \
                                blue += (px & blueMask) >> blueShift; \
                                ++colors; \
                            } \
                        } \
                        co2 += imageSize.Width - (right - left + 1); \
                    } \
                    if (colors > 0) \
                    { \
                        pixels[co] &= (pixel_type)alphaMask; \
                        pixels[co] |= (pixel_type)((red / colors) << redShift); \
                        pixels[co] |= (pixel_type)((green / colors) << greenShift); \
                        pixels[co] |= (pixel_type)((blue / colors) << blueShift); \
                    } \
                } \
                ++co; \
            } \
            co += imageSize.Width - areaWidth; \
        } \
        /* if rect is smaller than texture, copy rightmost/bottom col/row (and pixel at w,h) */ \
        /* as is from the inner one so that linear sampling works like clamp_to_edge */ \
        if ((I32)areaWidth < imageSize.Width) \
        { \
            for (y = 0; y < (I32)areaHeight; ++y) \
                pixels[y * imageSize.Width + areaWidth] = pixels[y * imageSize.Width + areaWidth - 1]; \
        } \
        if ((I32)areaHeight < imageSize.Height) \
        { \
            for (x = 0; x < (I32)areaWidth; ++x) \
                pixels[areaHeight * imageSize.Width + x] = pixels[(areaHeight - 1) * imageSize.Width + x]; \
        } \
        if ((I32)areaWidth < imageSize.Width && (I32)areaHeight < imageSize.Height) \
            pixels[areaHeight * imageSize.Width + areaWidth] = pixels[(areaHeight - 1) * imageSize.Width + areaWidth - 1];
// PROCESS_IMAGE define ends

    //const u32 bytesPerPixel = image->getBytesPerPixel();
    if (image->getColorFormat() == video::ECF_A8R8G8B8)
    {
        PROCESS_IMAGE(u32)
    }
    else if (image->getColorFormat() == video::ECF_A1R5G5B5)
    {
        PROCESS_IMAGE(u16)
    }

#undef PROCESS_IMAGE

    if (insetBorderClamp)
    {
        // when having forced inset border pixels, copy the columns/rows from the actual borders
        for (y = 1; y < (I32)areaHeight - 1; ++y)
        {
            image->setPixel(0, y, image->getPixel(1, y));
            image->setPixel(areaWidth - 1, y, image->getPixel(areaWidth - 2, y));
        }
        for (x = 0; x < (I32)areaWidth; ++x)
        {
            image->setPixel(x, 0, image->getPixel(x, 1));
            image->setPixel(x, areaHeight - 1, image->getPixel(x, areaHeight - 2));
        }
    }

    image->unlock();

    /*
    // test code to see all pixels with full opacity
    for (y = 0; y < (I32)areaHeight; ++y)
    {
        for (x = 0; x < (I32)areaWidth; ++x)
        {
            //pixels[y * w + x] |= alphaMask;
            video::SColor color = image->getPixel(x, y);
            color.setAlpha(0xff);
            image->setPixel(x, y, color);
        }
    }
    */
}


bool GraphicsIrr::createTexture(const C8 *name, video::ITexture *&oTexture,
                                core::vector2df *oUV, video::IImage *image,
                                UI32 flags, PixelRectangle *area)
{
    assert(oUV != 0);
    video::IVideoDriver *driver = mIrrDevice->getVideoDriver();
    bool orgMipMapFlag = driver->getTextureCreationFlag(video::ETCF_CREATE_MIP_MAPS);
    bool orgQualityFlag = driver->getTextureCreationFlag(video::ETCF_OPTIMIZED_FOR_QUALITY);
    bool orgNoAlphaFlag = driver->getTextureCreationFlag(video::ETCF_NO_ALPHA_CHANNEL);
    if (flags & IF_FILTER_LINEAR_MIPMAP)
        flags |= IF_USE_MIPMAPS;
    driver->setTextureCreationFlag(video::ETCF_CREATE_MIP_MAPS, (flags & IF_USE_MIPMAPS) != 0);
    driver->setTextureCreationFlag(video::ETCF_OPTIMIZED_FOR_QUALITY, true);
    driver->setTextureCreationFlag(video::ETCF_NO_ALPHA_CHANNEL, false);
    video::IImage *textureImage = image;
    bool dropTextureImage = false;
    const irrdimension2d &imageSize = image->getDimension();
    UI32 width = imageSize.Width, height = imageSize.Height, x = 0, y = 0;
    if (area != 0)
    {
        //textureImage = driver->createImage(image, core::position2d<s32>(area->x, area->y), core::dimension2d<s32>(area->w, area->h));
        //dropTextureImage = true;
        x = area->x;
        y = area->y;
        width = area->width;
        height = area->height;
    }
    bool makeInsetBorderClamp = (flags & IF_WRAP_CLAMP_EDGE) != 0;
    UI32 textureWidth, textureHeight;
    if (makeInsetBorderClamp)
    {
        textureWidth = sNextPowerOfTwo(width + 2);
        textureHeight = sNextPowerOfTwo(height + 2);
    }
    else
    {
        textureWidth = sNextPowerOfTwo(width);
        textureHeight = sNextPowerOfTwo(height);
    }
    if (imageSize.Width != textureWidth ||
        imageSize.Height != textureHeight ||
        imageSize.Width != width ||
        imageSize.Height != height)
    {
        dropTextureImage = true;
        textureImage = driver->createImage(video::ECF_A8R8G8B8, irrdimension2d(textureWidth, textureHeight));
        textureImage->fill(video::SColor(0));
        core::rect<s32> srcRect(core::position2d<s32>(x, y), core::dimension2d<s32>(width, height));
        image->copyTo(textureImage, makeInsetBorderClamp ? core::position2d<s32>(1, 1) : core::position2d<s32>(0, 0), srcRect);
    }
    sFixAlphaAndBorder_Irr(driver, textureImage, makeInsetBorderClamp, width, height);
    oTexture = driver->addTexture(name, textureImage);
    if (dropTextureImage)
        textureImage->drop();
    if (makeInsetBorderClamp)
    {
        float tex1pxw = 1.0f / textureWidth, tex1pxh = 1.0f / textureHeight;
        oUV[0].set(tex1pxw, tex1pxh);
        oUV[1].set((f32)width / textureWidth + tex1pxw, (f32)height / textureHeight + tex1pxh);
    }
    else
    {
        oUV[0].set(0, 0);
        oUV[1].set((f32)width / textureWidth, (f32)height / textureHeight);
    }
    driver->setTextureCreationFlag(video::ETCF_CREATE_MIP_MAPS, orgMipMapFlag);
    driver->setTextureCreationFlag(video::ETCF_OPTIMIZED_FOR_QUALITY, orgQualityFlag);
    driver->setTextureCreationFlag(video::ETCF_NO_ALPHA_CHANNEL, orgNoAlphaFlag);
    return oTexture != 0;
}


} // namespace turska
#endif // T2_USE_IRR
